package view;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;


/**
 * An internal standardized data structure for lists of data input into the view
 * package. This class allows the data passed to the view to be preserved in its
 * original type. This is important for java.util.Date, because when its value
 * is scaled against a minimum and maximum range, it must be converted to a
 * floating point value. However, when the value is printed on the axis labels,
 * it must be converted to a string to prevent showing the user the long integer
 * format of Date.
 * 
 * This class was included in view (rather than in model) due to a
 * misunderstanding in the original design goals of the project. In our API
 * design, we assumed that the passing of a data wrapper class between model and
 * view would violate good MVC design. Having a class or interface proprietary
 * to both view and model would not allow them to be used independently (e.g.
 * swapping two teams' model and view packages). The use of such a class,
 * however, would eliminate the need for the DataVector class in view, and
 * simplify the code tremendously. This major refactoring could not be achieved
 * within the assignment window, because our team was not aware of the
 * relaxation of complete package separation until very late in development.
 * 
 * @author geo4
 *         edited by Zachary Hopping
 * 
 * @param <T> a generic type that is comparable to itself
 */
public class DataVector<T extends Comparable<T>> {
    private static final String UNSUPPORTED_DATA_TYPE_ERROR_MESSAGE = "Unsupported data type";

    private ArrayList<T> myData;
    private String myLabel = "";

    private float myRangeMinimum;
    private float myRangeMaximum;

    /**
     * Constructor takes of a List containing elements implementing Comparable.
     * 
     * @param data list of elements of type comparable
     */
    public DataVector (List<T> data) {
        myData = new ArrayList<T>(data);
        for (T e : data) {
            if (!isSupportedDataType(e)) { throw new ViewException(
                                                                   UNSUPPORTED_DATA_TYPE_ERROR_MESSAGE,
                                                                   e); }
        }
        myRangeMaximum = convertSupportedTypeToFloat(Collections.max(myData));
        myRangeMinimum = convertSupportedTypeToFloat(Collections.min(myData));
    }

    /**
     * Gets the object stored at index i after being converted to a float.
     * 
     * @param i index of the object
     * @return objects value as represented by a float
     */
    public float getAsFloat (int i) {
        return convertSupportedTypeToFloat(myData.get(i));
    }

    /**
     * Sets the name describing the DataVector.
     * 
     * @param label String value naming the vector
     */
    public void setLabel (String label) {
        myLabel = label;
    }

    /**
     * Gets the name of the DataVector.
     * 
     * @return name of the DataVector. "" by default
     */
    public String getLabel () {
        return myLabel;
    }

    /**
     * Returns the maximum value to be plotted.
     * 
     * @return maximum value to be plotted as a float
     */
    public float getRangeLowerBound () {
        return myRangeMinimum;
    }

    /**
     * Returns the minimum value to be plotted.
     * 
     * @return minimum value to be plotted as a float
     */
    public float getRangeUpperBound () {
        return myRangeMaximum;
    }

    /**
     * Finds the minimum value within the vector's specified range and returns
     * it a String.
     * 
     * @return minimum value in the plotted range as a String
     */
    public String getSmallestElementInRangeAsString () {
        ArrayList<T> sortedData = new ArrayList<T>(myData);
        Collections.sort(sortedData);
        for (int i = 0; i < sortedData.size(); i++) {
            if (getAsFloat(i) >= myRangeMinimum) { return elementToString(i); }
        }
        return "";
    }

    /**
     * Finds the maximum value within the vector's specified range and returns
     * it a String.
     * 
     * @return maximum value in the plotted range as a String
     */
    public String getLargestElementInRangeAsString () {
        ArrayList<T> sortedData = new ArrayList<T>(myData);
        Collections.sort(sortedData);
        for (int i = sortedData.size() - 1; i >= 0; i--) {
            if (getAsFloat(i) <= myRangeMaximum) { return elementToString(i); }
        }
        return "";
    }

    /**
     * Sets the minimum value to be plotted.
     * 
     * @param newMinimum new minimum value as a float
     */
    public void setRangeLowerBound (float newMinimum) {
        myRangeMinimum = newMinimum;
    }

    /**
     * Sets the maximum value to be plotted.
     * 
     * @param newMaximum new maximum value as a float
     */
    public void setRangeUpperBound (float newMaximum) {
        myRangeMaximum = newMaximum;
    }

    /**
     * Checks if the element at the given index is within the set range.
     * 
     * @param index index of the element in question
     * @return true if the element is within plotting range of the vector.
     */
    public boolean elementIsWithinRange (int index) {
        return getAsFloat(index) >= myRangeMinimum && getAsFloat(index) <= myRangeMaximum;
    }

    /**
     * Returns the number of elements in the DataVector.
     * 
     * @return length of the DataVector
     */
    public int size () {
        return myData.size();
    }

    /**
     * Permanently sorts the elements of DataVector
     */
    public void sort () {
        Collections.sort(myData);
    }

    private String dateToShortFormatString (Date date) {
        return new SimpleDateFormat("MMM, yyyy").format(date);
    }

    /**
     * Returns the element at the specified index as a string.
     * 
     * @param index location of the element of interest
     * @return the element represented as a string.
     */
    private String elementToString (int index) {
        if (myData.get(index) instanceof Number) {
            return ((Number) myData.get(index)).toString();
        }
        else if (myData.get(index) instanceof Date) {
            return dateToShortFormatString((Date) myData.get(index));
        }
        else {
            throw new ViewException(UNSUPPORTED_DATA_TYPE_ERROR_MESSAGE);
        }
    }

    /**
     * Converts elements of the supported data types to a float.
     * 
     * @param element element from the vector
     * @return input element as a float
     */
    private float convertSupportedTypeToFloat (T element) {
        if (element instanceof Number) {
            return ((Number) element).floatValue();
        }
        else if (element instanceof Date) {
            return ((Date) element).getTime();
        }
        else {
            throw new ViewException(UNSUPPORTED_DATA_TYPE_ERROR_MESSAGE);
        }
    }

    /**
     * Determines if a generic element is of a data type supported by
     * DataVector. These are Numbers and Dates.
     * 
     * @param element generic comparable to check
     * @return if the element is of a supported data type
     */
    private boolean isSupportedDataType (T element) {
        return (element instanceof Number) || (element instanceof Date);
    }
}
